feat(auth): TOTP two-factor authentication (flag-gated rollout) - #22
Merged
Conversation
Adds authenticator-app 2FA to the local email+password auth. Sign-in becomes two-step: the password step returns a short-lived MFA challenge token instead of a session, and /auth/2fa/verify exchanges a 6-digit code (or a one-time recovery code) for the real access + refresh tokens. Backend: - migration 019_user_totp.sql: totp_secret/enabled/recovery_codes/enrolled_utc on users (recovery codes stored as bcrypt hashes, never plaintext). - app/core/totp.py: pyotp wrappers, recovery codes, and the challenge token (signed with a namespaced secret so it can't be replayed as a session token). - /auth/2fa/enroll, /auth/2fa/verify, GET /auth/2fa/status; signin AND signup gate behind PB_REQUIRE_2FA. An already-enrolled user is always challenged, even if the flag is off. Code step has its own brute-force lockout bucket. Frontend: - api client: signin/signup return AuthResult (session | MFA challenge); enroll2fa/verify2fa added. - AuthForm: 2FA step (manual setup key + otpauth deep link, mobile-friendly) and a one-time recovery-codes screen before routing in. Rollout is safe by default: PB_REQUIRE_2FA defaults OFF, so this deploys with enrollment available but not enforced. Flip it on after enrolling. Gates: ruff, mypy (132 files), red-team eval (0), 43 auth + 7 new 2FA tests, web typecheck/lint/build + 132 web tests all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds authenticator-app two-factor authentication to the local email+password auth. Required for all users via the
PB_REQUIRE_2FAflag, which defaults OFF so this deploys safely (enrollment available, not yet enforced).Sign-in becomes two-step: the password step returns a short-lived MFA challenge token instead of a session, and
/auth/2fa/verifyexchanges a 6-digit code (or a one-time recovery code) for the real access + refresh tokens.Backend
019_user_totp.sql:totp_secret/totp_enabled/totp_recovery_codes/totp_enrolled_utconusers. Recovery codes stored as bcrypt hashes, never plaintext.app/core/totp.py:pyotpwrappers, recovery-code gen/verify, and the challenge token (signed with a namespaced secret so it can never be replayed as a session token).POST /auth/2fa/enroll,POST /auth/2fa/verify,GET /auth/2fa/status. Both/auth/signinand/auth/signupgate behindPB_REQUIRE_2FA. An already-enrolled user is always challenged even if the flag is off. The code step has its own brute-force lockout bucket.Frontend
signin/signupnow returnAuthResult(session or MFA challenge);enroll2fa/verify2faadded.AuthForm: a 2FA step (manual setup key +otpauth://deep link, chosen over a QR because this is a mobile-first app where you can't scan your own screen) and a one-time recovery-codes screen before routing in.Rollout (safe by default)
PB_REQUIRE_2FAoff (this PR's default). Nothing changes for existing users yet.PB_REQUIRE_2FA=truein Render to enforce for everyone.Note: existing refresh tokens continue to work until they cycle; enforcement applies at each fresh sign-in.
Verification
tests/test_2fa.pyall pass.test_streaming_run_emits_progress_sources_and_completion) is order-dependent and flaky in the full run (passes in isolation); not touched by this change.🤖 Generated with Claude Code